package com.hhs.FileUpload.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hhs.FileUpload.entity.FileInfo;
import com.hhs.FileUpload.entity.FileInfoDTO;
import com.hhs.FileUpload.entity.FileVerify;
import com.hhs.FileUpload.mapper.FileMapper;
import com.hhs.FileUpload.service.FileService;

//import com.hhs.FileUpload.util.UploadUtil;
import com.hhs.FileUpload.util.IpAddr;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Objects;

@Slf4j
@Service
public class FileServiceImpl implements FileService {

    @Autowired
    private FileMapper fileMapper;


    @Value("${file.fenpianLocation}")
    private String uploadPath;

    // 端口号
    @Value("${server.port}")
    private Integer port;

    // 虚拟路径的地址
    @Value("${virtualFileDepositPath}")
    private String virtualFileDepositPath;

    // 每 1MB 作为一个分片
    private int shardSizeInt = 1 * 1024 * 1024;

    /**
     * explain : 默认上传文件接口
     *
     * @param fileInfoDTO 文件
     */
//    @Override
//    @Transactional(rollbackFor = Exception.class)
//    public String defaultUpload(FileInfoDTO fileInfoDTO) {
//        Assert.notNull(fileInfoDTO.getFile(), "请选择需要上传文件..");
//        FileInfo fileInfo = new FileInfo();
//        try {
//            String fileUrl = UploadUtil.upLoaderFile(fileInfoDTO.getFile());
//            fileInfo.setFileUrl(fileUrl);
//            //构建文件信息
//            fileInfo = getFileInfoBy(fileInfoDTO, fileInfo);
//        } catch (Exception e) {
//            log.info("上传文件失败..", e);
//            throw new IllegalArgumentException("默认上传文件失败..");
//        }
//        fileMapper.insert(fileInfo);
//        return fileInfo.getFileId();
//    }

    /**
     * explain : 构建文件信息
     *
     * @param fileInfoDTO 文件传参
     * @param fileInfo    文件信息

     */
    private FileInfo getFileInfoBy(FileInfoDTO fileInfoDTO, FileInfo fileInfo) {
        fileInfo.setBusinessId(fileInfoDTO.getBusinessId());
        fileInfo.setBusMode(fileInfoDTO.getBusMode());

        MultipartFile file = fileInfoDTO.getFile();
        String originalFilename = file.getOriginalFilename();
        fileInfo.setFileName(originalFilename);
        fileInfo.setFileType(FileNameUtil.getSuffix(originalFilename));

        return fileInfo;
    }


    /**
     * explain : 默认下载接口
     *
     * @param fileId   文件ID
     * @param request  请求
     * @param response 响应
     *                 */

    public void defaultDownload(String fileId, HttpServletRequest request, HttpServletResponse response) {
        FileInfo fileInfo = fileMapper.selectById(fileId);
        Assert.notNull(fileInfo, "未找到相关文件信息.");
        String fileUrl = fileInfo.getFileUrl();
       // MinioUtils.me().downloadFileFromMinio(fileUrl, request, response, fileInfo.getFileName());
    }

    /**
     * explain : 文件分页接口
     *
     * @param fileInfoDTO 查询参数
*/
    public Page<FileInfo> filePage(FileInfoDTO fileInfoDTO) {
        LambdaQueryWrapper<FileInfo> lambdaedQuery = Wrappers.lambdaQuery(new FileInfo());
        lambdaedQuery.like(StrUtil.isNotBlank(fileInfoDTO.getFileName()), FileInfo::getFileName, fileInfoDTO.getFileName());

        return null;
//        return PageHelper.startPage(fileInfoDTO.getPageNum(), fileInfoDTO.getPageSize())
//                .doSelectPageInfo(() -> fileMapper.selectList(lambdaedQuery));
    }

    /**
     * explain : 验证当前文件可以分成几片,以及 返回MD5加密字符串
     *
     * @param fileVerify 校验文件信息
     * @return com.hulunbuir.study.infra.domain.FileVerify
     * @author wangjunming > qjyn1314@163.com
     * @since 2023/11/8 9:14
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public FileVerify verifyFile(FileVerify fileVerify) {
        // 校验文件时将保存文件信息到 数据库中.
        log.info("fileVerify-->{}", fileVerify);
        LambdaQueryWrapper<FileInfo> queryWrapper = Wrappers.lambdaQuery(new FileInfo());
        queryWrapper.eq(StrUtil.isNotBlank(fileVerify.getChunkMd5()), FileInfo::getChunkMd5, fileVerify.getChunkMd5());
        queryWrapper.eq(StrUtil.isNotBlank(fileVerify.getFileId()), FileInfo::getFileId, fileVerify.getFileId());

        FileInfo existFile = fileMapper.selectOne(queryWrapper);
        // 在编辑文件的时, 其 文件ID和文件MD5加密都在传参中,此时还是没有找到文件, 则抛出异常.
        Assert.isFalse(StrUtil.isNotBlank(fileVerify.getChunkMd5()) && StrUtil.isNotBlank(fileVerify.getFileId()) && Objects.isNull(existFile), "文件ID和文件MD5加密都在传参中,此时还是没有找到文件, 则抛出异常.");
        if (Objects.isNull(existFile)) {
            // 将文件信息保存到数据库中 并返回文件ID和MD5
            existFile = getFileInfoByVerifyFile(fileVerify);
            fileMapper.insert(existFile);
        }
        fileVerify.setFileId(existFile.getFileId());
        fileVerify.setChunkMd5(existFile.getChunkMd5());
        fileVerify.setChunkFlag(existFile.getChunkFlag());
        fileVerify.setShardTotal(existFile.getShardTotal());
        fileVerify.setShardSize(existFile.getShardSize());
        fileVerify.setShardIndex(existFile.getShardIndex());

        if (existFile.getShardIndex().equals(existFile.getShardTotal())) {
            fileVerify.setChunkFlag(Boolean.TRUE);
        }

        fileVerify.setFile(null);

        // 生成文件分片数值列表
        List<FileVerify.ChunkShard> shardList = getShardList(fileVerify);
        fileVerify.setShardList(shardList);
        fileVerify.setEndFlag(CollUtil.isEmpty(shardList));

        return fileVerify;
    }

    /**
     * 构建所需要对文件进行截取的分片开始值和结束值, 用于前端 for循环调用分片上传接口
     */
    private List<FileVerify.ChunkShard> getShardList(FileVerify fileVerify) {
        BigDecimal shardTotal = fileVerify.getShardTotal();
        List<FileVerify.ChunkShard> shardList = CollUtil.newArrayList();

        for (int i = fileVerify.getShardIndex().intValue(); i < shardTotal.intValue(); i++) {
            FileVerify.ChunkShard chunkShard = new FileVerify.ChunkShard();
            chunkShard.setShardTotal(fileVerify.getShardTotal());
            chunkShard.setShardIndex(BigDecimal.valueOf(i));
            chunkShard.setShardStart(BigDecimal.valueOf(i).multiply(fileVerify.getShardSize()));
            chunkShard.setShardEnd(BigDecimal.valueOf(i).multiply(fileVerify.getShardSize()).add(fileVerify.getShardSize()));
            if (i == shardTotal.intValue() - 1) {
                //如果是最后一个分片则对截取文件的大小
                chunkShard.setShardEnd(BigDecimal.valueOf(fileVerify.getFileSize()));
            }
            shardList.add(chunkShard);
        }

        return shardList;
    }



    /**
     * 封装校验文件接口所需要的参数信息
     */
    private FileInfo getFileInfoByVerifyFile(FileVerify fileVerify) {
        FileInfo fileInfo = new FileInfo();
        fileInfo.setBusinessId(fileVerify.getBusinessId());
        fileInfo.setBusMode(fileVerify.getBusMode());
        fileInfo.setFileName(fileVerify.getFileName());
        fileInfo.setFileType(FileNameUtil.getSuffix(fileVerify.getFileName()));
        fileInfo.setFileSize(BigDecimal.valueOf(fileVerify.getFileSize()));

        fileInfo.setServerFileName(UUID.randomUUID().toString(true).toUpperCase() + "." + fileInfo.getFileType());

        fileInfo.setChunkMd5(fileVerify.getChunkMd5());
        fileInfo.setChunkFlag(Boolean.TRUE);
        fileInfo.setChunkFolder(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")) + "/" + RandomUtil.randomString(6).toUpperCase());

        fileInfo.setShardIndex(BigDecimal.ZERO);
        // 以 每 20MB进行上传文件
        // 字节数除以20MB, 即 除以 20*1024*1024
        BigDecimal shardSize = BigDecimal.valueOf(shardSizeInt);
        fileInfo.setShardSize(shardSize);
        BigDecimal shardTotal = fileInfo.getFileSize().divide(shardSize, 0, RoundingMode.HALF_UP);
        fileInfo.setShardTotal(shardTotal);

        return fileInfo;
    }


    /**
     * explain : 分片上传文件接口
     *
     * @param fileVerify 文件校验信息

     */

    @Transactional(rollbackFor = Exception.class)
    public FileVerify breakpointUpload(FileVerify fileVerify) {
        Assert.notBlank(fileVerify.getFileId(), "文件ID不能为空.");
        FileInfo existFile = fileMapper.selectById(fileVerify.getFileId());
        Assert.notNull(existFile, "未找到此文件信息");
        fileVerify.setShardTotal(existFile.getShardTotal());

        // 上传文件
        MultipartFile file = fileVerify.getFile();
        // 自定义分片的名称以及上传路径
        String shardFileName = getShardFileName(existFile, fileVerify.getShardIndex());
        // 将对应的内容上传到 对应的位置
        upLoaderShardFile(file,existFile.getChunkFolder(), shardFileName);
        log.info("上传成功的分片文件名是->{}", shardFileName);
        // 更新文件信息
        FileInfo fileInfo = new FileInfo();
        fileInfo.setFileId(fileVerify.getFileId());
        // 更新上传的文件分片下标
        fileInfo.setShardIndex(fileVerify.getShardIndex());
        fileMapper.updateById(fileInfo);
        log.info("更新文件分片信息到数据库-->{}", JSONUtil.toJsonStr(fileInfo));
        fileVerify.setFile(null);
        // 如果上传的分片下标正好等于 (总数的-1) 则认为是最后一个文件的分片已经上传成功了. 可以进行对所有的分片文件进行合并.
        if (fileVerify.getShardIndex().equals(existFile.getShardTotal().subtract(BigDecimal.ONE))) {
            fileVerify.setEndFlag(Boolean.TRUE);
            // 开始进行对分片文件进行合并
            log.info("此处所有分片文件都已上传, 进行合并文件...");
            mergeShardFile(existFile);
        }
        return fileVerify;
    }

    /**
     * 将分片的文件保存到对应的路径里面
     * @param file
     * @param shardFileName
     */

    private void upLoaderShardFile(MultipartFile file,String folder, String shardFileName) {
        //
        String filePath=uploadPath+File.separator+folder;
        File file1 = new File(filePath);
        if(!file1.exists()){
            file1.mkdirs();
        }
        System.out.println(filePath);
        System.out.println(shardFileName);
        //
        File dest = new File(filePath, shardFileName);
        try {
            file.transferTo(dest);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 将多个分片文件进行合并.
     */
    private void mergeShardFile(FileInfo existFile) {
        // 获取所有的分片文件流,进行组装为一个流, 并写入到目的路径中.
        BigDecimal shardTotal = existFile.getShardTotal();
        byte[] allFileByte = new byte[existFile.getFileSize().intValue()];
        int startLength = 0;
        for (int i = 0; i < shardTotal.intValue(); i++) {
            existFile.setShardIndex(BigDecimal.valueOf(i));
            // 构建 文件服务器中的所有分片文件的名称
            // 通过文件名从文件服务器中获取文件流, 进行合并到一个文件流中.
            String fileName = getShardFileName(existFile, BigDecimal.valueOf(i));

            byte[] fileByte = getByteByName(existFile,fileName);
            /**
             * System.arraycopy(src, srcPos, dest, destPos, length)
             * 参数解析：
             * src：byte源数组
             * srcPos：截取源byte数组起始位置（0位置有效）
             * dest,：byte目的数组（截取后存放的数组）
             * destPos：截取后存放的数组起始位置（0位置有效）
             * length：截取的数据长度
             */
            System.arraycopy(fileByte, 0, allFileByte, startLength, fileByte.length);
            startLength = startLength + fileByte.length;
        }
        InputStream inputStream = new ByteArrayInputStream(allFileByte);
        // 然后将文件流上传至服务器
        // 指定存储的文件名称
        String finalFileName = getFinalFileName(existFile);
        // 进行上传文件

        // 根据输入流将文件上传到指定位置
        upLoaderFileByByte(inputStream,existFile,finalFileName);
        log.info("上传成功的分片文件名是->{}", finalFileName);
        // 上传完之后将分片的文件删除
        // 更新文件信息
        FileInfo fileInfo = new FileInfo();
        fileInfo.setFileId(existFile.getFileId());
        /**
         * 存储文件的名字
         */
         String fileUrl="http://"+IpAddr.getServerIp()+":"+port+virtualFileDepositPath+existFile.getChunkFolder()+File.separator+finalFileName;
        //System.out.println(fileUrl);
        log.info("上传成功之后文件的名字->{}",fileUrl);





        // 更新上传的文件分片下标
        fileInfo.setFileUrl(finalFileName);
        log.info("更新成功数据-->{}", JSONUtil.toJsonStr(fileInfo));
        fileMapper.updateById(fileInfo);
    }

    /**
     * 根据文件的输入流将文件上传到指定的位置
     * @param inputStream
     * @param existFile
     * @param finalFileName
     */

    private void upLoaderFileByByte(InputStream inputStream, FileInfo existFile, String finalFileName) {
        // 创建File对象，表示目标文件
        String filePath=uploadPath+File.separator+existFile.getChunkFolder();
        File targetFile = new File(filePath,finalFileName);

        // 使用try-with-resources语句自动关闭资源
        try (FileOutputStream fos = new FileOutputStream(targetFile)) {
            // 创建一个缓冲区
            byte[] buffer = new byte[1024];
            int bytesRead;

            // 从InputStream中读取数据，直到没有更多数据可读
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                // 将读取的数据写入到FileOutputStream中
                fos.write(buffer, 0, bytesRead);
            }

            // 注意：由于使用了try-with-resources，所以不需要显式调用fos.close()
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        // 如果需要，可以在这里处理文件写入后的逻辑
        // 例如，输出一条消息表示文件已成功写入
        System.out.println("文件已成功写入到: " + uploadPath+File.separator+finalFileName);
    }

    /**
     * 生成最终的需要的文件名+路径
     */
    private String getFinalFileName(FileInfo existFile) {
        //return existFile.getChunkFolder() + "/" + existFile.getServerFileName();
        return  existFile.getServerFileName();
    }

    /**
     * 生成文件分片名称+路径
     */
    private String getShardFileName(FileInfo fileInfo, BigDecimal shardIndex) {
        if (Objects.isNull(shardIndex)) {
           // return fileInfo.getChunkFolder() + "/" + fileInfo.getServerFileName();
            return  fileInfo.getServerFileName();
        }
        //return fileInfo.getChunkFolder() + "/" + fileInfo.getServerFileName() + "." + shardIndex;
        return  fileInfo.getServerFileName() + "." + shardIndex;
    }

    /**
     * 获取文件的字节流
     */
    public byte[] getByteByName(FileInfo existFile, String fileName){
        byte[] fileContent = new byte[0];
        String filePath=uploadPath+File.separator+existFile.getChunkFolder()+File.separator+fileName;
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filePath));

             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {

            byte[] buffer = new byte[1024000]; // 使用一个更合理的缓冲区大小
            int bytesRead;
            while ((bytesRead = bis.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesRead);
            }

            fileContent= baos.toByteArray(); // 现在fileContent包含了文件的全部内容  

        } catch (IOException e) {

            e.printStackTrace();

        }
        return fileContent;
    }


//    @Override
    public Boolean remove(String fileId) {
        Assert.notBlank(fileId, "文件ID为空. ");
        FileInfo fileInfo = fileMapper.selectById(fileId);
        Assert.notNull(fileInfo, "根据文件ID未找到文件信息. ");

        // 删除 对象存储库中的源文件
        String finalFileName = getFinalFileName(fileInfo);
        // 1.删除合并后文件
        //MinioUtils.me().delFile(finalFileName);
        // 删除 对象存储库中的分片源文件
        for (int i = 0; i < fileInfo.getShardTotal().intValue(); i++) {
            String shardFileName = getShardFileName(fileInfo, BigDecimal.valueOf(i));
            // 2. 删除分片的文件
            //MinioUtils.me().delFile(shardFileName);
        }
        return fileMapper.deleteById(fileId) > 0;
    }

}
